深入理解 async await—concurrentAsync vs sequenceAsync
2022-08-10 Wed
當一個 function 附帶有async的關鍵字的時候,他被稱之為async function,透過async function將可讓await 關鍵字寫在裡面,反之如果不是在async function裡面撰寫await關鍵字的話將會沒有作用,另外透過await的關鍵字可以使其非同步操作,也可以更簡潔的方式撰寫promise的行為,換句話說可以免於撰寫promise chain。
以上翻譯自 MDN 並加入一些個人字詞以便使字句更通順 參考資料
原本 promise 寫法
首先我們可以先觀看原本 promise 的寫法再對照 觀看以下程式碼
function resolveAfter1Seconds() {
return new Promise((resolve) => {
setTimeout(() => {
resolve("resolved");
}, 1000);
});
}
// 使用prmise;
resolveAfter2Seconds().then((response) => {
console.log("calling");
console.log(response);
console.log("finally");
});我們建立一個 function 將會回傳建立出一個Promise 物件,透過setTimeoutAPI在一秒後將執行**resolve()**的函式
按照順序將會印出
calling
resolved
finally
async await 寫法
如果換成 async 的方式改寫的話如下
async function asyncCall() {
console.log("calling");
const result = await resolveAfter2Seconds();
console.log(result);
console.log("finally");
}
asyncCall();在 function 添加 async 關鍵字後以及在 promise 物件前面添加 await 關鍵字即可得到 resolve 的值,讓我們不用撰寫 then
async 回傳一個 promise
console.log(asyncCall());我們嘗試著印出 asyncCall()的執行結果可以發現 console 顯示 Promise,因此 async function 呼叫後回傳是一個 promise 物件

因此我們當然可以使用 promise 本身.then 的方式串接 asyncCall()
觀看以下範例
function randomNumberToMakeBigOrSmallString() {
return new Promise((resolve) => {
setTimeout(() => {
let randomNumber = Math.floor(Math.random() * 10);
if (randomNumber >= 5) {
resolve("大");
} else {
resolve("小");
}
}, 1000);
});
}
async function asyncCall() {
const BigOrSmall = await randomNumberToMakeBigOrSmallString();
return BigOrSmall;
}
asyncCall().then((response) => {
console.log("你的幸運數字偏" + response);
});我們隨機產生一個 1~10 的數字,如果大於等於 5 的話就回傳"大",這邊使用.then 串接裡面的 callback function 接收一個參數來自於 asyncCall 的 return 值。
但通常用了 async await 後就應當避免使用.then 的寫法繼續串接下去。
使用 IIFE 搭配 async await
可以使用 IIFE(Immediately Invoked Function Expression)立即執行函式搭配 async 和 await
function randomNumberToMakeBigOrSmallString() {
return new Promise((resolve) => {
setTimeout(() => {
let randomNumber = Math.floor(Math.random() * 10);
if (randomNumber >= 5) {
resolve("大");
} else {
resolve("小");
}
}, 1000);
});
}
async function asyncCall() {
const BigOrSmall = await randomNumberToMakeBigOrSmallString();
return BigOrSmall;
}
(async () => {
console.log("你的幸運數字偏" + (await asyncCall()));
})();錯誤處理
由於使用 async await 的 funciton 的前提是讓非同步的 code 轉換成類似同步的寫法,以往撰寫 promise 使用.then 的時候可以藉由.then 的第二個參數或是撰寫.catch 裡面的 callback 進行錯誤處理。如下
function getNumber(number) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (number >= 5) {
resolve("大");
} else {
reject("太小了");
}
}, 1000);
});
}
getNumber(3)
.then((response) => {
console.log("你輸入的數字是"+response);
})
.catch((response) => {
console.log("你輸入的數字是"+response);
});使用 async 搭配 try catch 錯誤處理
原本的範例我們可以透過 async 和 try catch 進行改寫,範例如下
function getNumber(number) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (number >= 5) {
resolve("大");
} else {
reject("太小了");
}
}, 1000);
});
}
(async () => {
try {
console.log("你輸入的數字是" + (await getNumber(3)));
} catch (error) {
console.log("無法成功的原因是" + error);
}
})();得知來自於某個 await 的錯誤
使用多個 try catch 接收各別 await 的錯誤
如果擁有多個 await 的時候想要得知是哪一行得使用多個 try catch,撰寫如下
(async () => {
try {
console.log("你輸入的數字是" + (await getNumber(3)));
} catch (error) {
console.log("無法成功的原因1是" + error);
}
try {
console.log("你輸入的數字是" + (await getNumber(2)));
} catch (error) {
console.log("無法成功的原因2是" + error);
}
})();使用 await 串接 catch
await 所接的 function 本質是得到一個 promise,因此也可以使用原先 promise 的 catch 串接,我們在 await 後面串接 catch 程式碼如下
(async () => {
let result1 = await getNumber(3).catch((error) => {
return 1 + error;
});
let result2 = await getNumber(1).catch((error) => {
return 2 + error;
});
console.log("無法成功的原因" + result1);
console.log("無法成功的原因" + result2);
})();async 搭配 promise all
使用 then 撰寫 promise all
撰寫以下範例 lateWakeUp 的函式在 2 秒後會執行 resolve 函式,earlyWakeUp 在 1 秒後執行 resolve 函式,使用 Promise all 的用法是將其放入可迭代的東西上(例如 array),之後在執行 Promise all 將會平行運算(Parallel Computing)。換句話說 lateWakeUp 和 earlyWakeUp 是同時開始執行,因此在 Promise.all 開始執行的時候,1 秒後將會印出 early done,之後再過 1 秒鐘就會印出 late done
function lateWakeUp() {
return new Promise((resolve) => {
setTimeout(() => {
resolve("I am late");
console.log("late done");
}, 2000);
});
}
function earlyWakeUp() {
return new Promise((resolve) => {
setTimeout(() => {
resolve("I am early");
console.log("early done");
}, 1000);
});
}
let early = earlyWakeUp();
let late = lateWakeUp();
Promise.all([early, late]).then((string) => console.log(string));使用 async await 撰寫 Promise all
方法一
以剛剛的範例撰寫成 asyanc await 的形式,其 all 函式裡面一樣放入陣列,在整個函式外面加上 async 的 keyword,在 Promise.all 的地方加上 await 一樣可以印出回傳值為一個陣列 程式碼如下
async function asyncWithPromiseAll() {
let response = await Promise.all([earlyWakeUp(), lateWakeUp()]);
console.log(response);
}
asyncWithPromiseAll();方法二
另外也可以在 Promise.all 函式裡面寫一個 IIFE(Immediately Invoked Function Expression)立即執行函式,範例如下
async function asyncWithPromiseAll() {
let response = await Promise.all([
(async () => {
return earlyWakeUp();
})(),
(async () => {
return lateWakeUp();
})(),
]);
console.log(response);
}
asyncWithPromiseAll();這樣寫的好處是可以在立即執行函式透過撰寫 await 關鍵字在函式裡面,當 resolve 時候印出其值,而透過 Promise all 也能平行運算,範例如下
async function asyncWithPromiseAll() {
await Promise.all([
(async () => {
console.log(await earlyWakeUp());
})(),
(async () => {
console.log(await lateWakeUp());
})(),
]);
}
asyncWithPromiseAll();concurrentAsync vs sequenceAsync
這裡將對比同時間發生的 async 和循序發生的 async
建立 reslove 函式和 reject 函式
以下程式碼將會先建立出一個 1 秒後才會執行 resolve 的 Promise 函式和 1 秒後才會執行的 reject 函式
function resolveFunction() {
return new Promise((resolve) => {
setTimeout(() => {
resolve("I am resolve");
console.log("resolve done");
}, 1000);
});
}
function rejectFunction() {
return new Promise((_, reject) => {
setTimeout(() => {
reject("I am reject");
console.log("reject done");
}, 1000);
});
}按順序呼叫(使用常見的方式寫 async 和 await)
這邊使用 Date.now()的方式來觀看執行的時間差,下面的範例將會依序的執行兩次 resolveFunction(),換句話說第三行呼叫完畢才會呼叫第四行
async function sequenceAsync() {
const start = Date.now();
let response1 = await resolveFunction();
let response2 = await resolveFunction();
console.log(`The response1 is ${response1}\nThe response2 is ${response2}\nIt is spend ${
Date.now() - start
} ms.
`);
}
sequenceAsync();執行結果如下圖

使用 async await 建立一個 Parallel
透過先呼叫建立 Promise 的函式,將其存入某個變數之後再使用 await 將其內容提取出,觀看以下程式碼
async function concurrentAsync() {
const start = Date.now();
const caller1 = resolveFunction();
const caller2 = resolveFunction();
let response1 = await caller1;
let response2 = await caller2;
console.log(
`The response1 is ${response1}\nThe response2 is ${response2}\nIt is spend ${
Date.now() - start
} ms.`
);
}
concurrentAsync();這邊可以看到由於我們在執行兩次 resolveFunction 的時候並沒有使用 await 關鍵字,而是在第 5 行和第 6 行的時候使用 await 關鍵字將其值給提取出來,最後計算時間可以發現,他們是幾乎同時呼叫了 resolveFunction
結果如下圖

concurrentAsync = Promise.all ?
既然 concurrentAsync 可以做到同時執行 resolveFunction,那我們是否可以用來替代 Promise.all 呢?
可以參考在 MDN 英文版的async function這個範例的 waring 部分,如果不太懂意思,在 stack overflow 上也有人針對 MDN 的 waring 部分提問連結如下
Promise rejection regardless of configuring catch clause on caller
另外一篇 stack overflow 也有人提問連結如下,這篇的範例講解可以清楚了解當 reject 的時候,執行結果的差異。
Any difference between await Promise.all() and multiple await?
Promise.all 定義是當引數(iterable)的所有 promise 被 resolved 或者第一個被拒絕的 promise,假設使用 concurrent Async 與 multiple await 做為 thread compute 出發,當第一個 reject 回來的時候會比較短。
參考資料
- Deeply Understanding JavaScript Async and Await with Examples
- What is Async/Await Good For
- Async function / Await 深度介紹
- Asynchronous Iterators for JavaScript
- for await...of
- [JS] Async and Await in JavaScript
- Asynchronous Functions 101
- Waiting for more than one concurrent await operation-stack- overflow
- Promise rejection regardless of configuring catch clause on caller
- Any difference between await Promise.all() and multiple await?
- JavaScript async and await in loops
- 如何在 JS 迴圈中正確使用 async 與 await